﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace DatabaseSimulation
{
    public abstract partial class Database
    {
        protected DatabaseObject[] DatabaseObjects { get; private set; }
        private HardDrive[] HardDrives { get; set; }
        private Cpu[] Cpus { get; set; }
        private List<Transaction> CurrentTransactions { get; set; }
        private TransactionGenerator TransactionGenerator { get; set; }
        protected SimulationResult CurrentSimulationResult { get; set; }

        public int NumberOfObjects { get; private set; }
        public int NumberOfHardDrives { get; private set; }
        public int NumberOfCpus { get; private set; }
        public int Milliseconds { get; private set; }

        public Database(TransactionGenerator transactionGenerator, int numberOfObjects, int numberOfHardDrives, int numberOfCpus, int milliseconds)
        {
            this.TransactionGenerator = transactionGenerator;
            this.NumberOfObjects = numberOfObjects;
            this.NumberOfHardDrives = numberOfHardDrives;
            this.NumberOfCpus = numberOfCpus;
            this.Milliseconds = milliseconds;
        }

        public SimulationResult RunSimulation(TransactionGenerator generator, int multiProgrammingLevel)
        {
            Transaction.ResetIdCounter();
            DatabaseObject.ResetIdCounter();
            InitializeDatabase();

            for (int i = 0; i < this.Milliseconds; i++)
            {
                for (int j = this.CurrentTransactions.Count; j < multiProgrammingLevel; j++)
                {
                    this.CurrentTransactions.Add(this.TransactionGenerator.GenerateTransaction(this.NumberOfObjects));
                }
                AdvanceSimulation();
            }

            SimulationComplete(multiProgrammingLevel);
            return this.CurrentSimulationResult;
        }

        private void InitializeDatabase()
        {
            this.CurrentSimulationResult = new SimulationResult();
            this.CurrentTransactions = new List<Transaction>();
            this.DatabaseObjects = new DatabaseObject[NumberOfObjects];
            this.HardDrives = new HardDrive[NumberOfHardDrives];
            this.Cpus = new Cpu[NumberOfCpus];

            for (int i = 0; i < this.DatabaseObjects.Length; i++)
            {
                this.DatabaseObjects[i] = new DatabaseObject();
            }

            for (int i = 0; i < this.HardDrives.Length; i++)
            {
                this.HardDrives[i] = new HardDrive();
            }

            for (int i = 0; i < this.Cpus.Length; i++)
            {
                this.Cpus[i] = new Cpu();
            }
        }

        private void AdvanceSimulation()
        {
            foreach (Transaction transaction in this.CurrentTransactions)
            {
                if (!transaction.Killed && transaction.Operations.Any(i => !i.Completed && !i.TempCompleted))
                {
                    //Phase 1: Read Phase
                    DatabaseOperation operation = transaction.Operations.FirstOrDefault(i => !i.Completed && !i.TempCompleted);

                    switch (operation.OperationType)
                    {
                        case DatabaseOperationType.Read:
                            {
                                HandleReadOperation(operation);
                                break;
                            }
                        case DatabaseOperationType.Write:
                            {
                                HandleWriteOperation(operation);
                                break;
                            }
                    }
                }
                else if (transaction.Operations.Any(i => !i.Completed))
                {
                    //Phase 2: Wait to Commit
                    if (!transaction.CanCommit && this.CheckCanCommitTransaction(transaction))
                    {
                        transaction.MarkCanCommit();
                    }

                    if (transaction.CanCommit)
                    {
                        //Phase 3: Commit Phase
                        DatabaseOperation operation = transaction.Operations.First(i => !i.Completed);
                        HandleWriteOperation(operation);
                    }
                }
                else
                {
                    throw new Exception("This should never happen");
                }

                if (transaction.Operations.All(i => i.Completed))
                {
                    if (transaction.Operations.All(i => i.OperationType == DatabaseOperationType.Read))
                    {
                        transaction.MarkCanCommit();
                    }
                    CommitTransaction(transaction);
                }
            }

            this.CurrentTransactions.RemoveAll(i => i.Killed || i.Completed);
        }

        private void CommitTransaction(Transaction transaction)
        {
            transaction.MarkComplete();
            RemoveReadingAndWriting(transaction);
            this.TransactionCommitted(transaction);
            this.CurrentSimulationResult.CompletedTransactions++;
            this.CurrentSimulationResult.UsefulCpuUtilization += transaction.Operations.Sum(i => i.ElapsedCpuTime);
            this.CurrentSimulationResult.UsefulIoUtilization += transaction.Operations.Sum(i => i.ElapsedIoTime);
        }

        private void RemoveReadingAndWriting(Transaction transaction)
        {
            foreach (DatabaseOperation operation in transaction.Operations)
            {
                DatabaseObject dbObject = this.DatabaseObjects[operation.ObjectID];
                dbObject.ReadingTransactions.Remove(transaction);
                dbObject.WritingTransactions.Remove(transaction);
            }
        }

        protected abstract void TransactionCommitted(Transaction transaction);

        protected abstract bool ReadAfterWriteConflict(DatabaseOperation operation);

        protected abstract bool WriteAfterReadConflict(DatabaseOperation operation);

        protected abstract bool CheckCanCommitTransaction(Transaction transaction);

        protected abstract void SimulationComplete(int mpl);

        #region Reading
        private void HandleReadOperation(DatabaseOperation operation)
        {
            if (operation.ElapsedIoTime == 0)
            {
                //Step 1: Begin Reading
                BeginRead(operation);
            }
            else if (operation.ElapsedIoTime < operation.IoTimeToComplete)
            {
                //Step 2: Continue Reading
                ContinueReading(operation);
            }
            else if (operation.ElapsedCpuTime == 0)
            {
                //Step 3: Begin Cpu Processing
                BeginCpuProcessing(operation);
            }
            else if (operation.ElapsedCpuTime < operation.CpuTimeToComplete)
            {
                ContinueCpuProcessing(operation);
            }

            if (operation.ElapsedCpuTime == operation.CpuTimeToComplete)
            {
                //Step 5: Operation Finished
                OperationFinished(operation);
            }
        }

        /// <summary>
        /// In handling reads, we must check for RAW conflicts
        /// </summary>
        private void BeginRead(DatabaseOperation operation)
        {
            bool canContinue = true;

            DatabaseObject dbObject = this.DatabaseObjects[operation.ObjectID];
            if (dbObject.ExclusiveTransaction != null)
            {
                canContinue = false;
                HandleExclusiveBlock(operation);
            }
            else if (dbObject.WritingTransactions.Any(i => i != operation.Transaction))
            {
                //Read After Write (RAW) Conflict
                canContinue = ReadAfterWriteConflict(operation);
            }
            
            if (canContinue)
            {
                //No Conflict
                if (AcquireHardDrive(operation))
                {
                    dbObject.ReadingTransactions.Add(operation.Transaction);
                    ContinueReading(operation);
                }
                else
                {
                    if (operation.ElapsedBlockedOnHardDriveTime == 0)
                    {
                        this.CurrentSimulationResult.HardDriveReadBlocks++;
                    }
                    operation.IncreaseBlockedOnHardDriveTime();
                    this.CurrentSimulationResult.BlockedOnHardDriveReadTime++;
                }
            }
        }

        protected abstract void HandleExclusiveBlock(DatabaseOperation operation);

        private void ContinueReading(DatabaseOperation operation)
        {
            operation.IncreaseIoTime();
            this.CurrentSimulationResult.IoUtilization++;

            if (operation.ElapsedIoTime == operation.IoTimeToComplete)
            {
                FinishRead(operation);
            }
        }

        private void FinishRead(DatabaseOperation operation)
        {
            ReleaseHardDrive(operation);
        }
        #endregion

        #region Writing
        private void HandleWriteOperation(DatabaseOperation operation)
        {
            if (operation.ElapsedCpuTime == 0)
            {
                //Step 1: Begin Cpu Processing
                BeginCpuProcessing(operation);
            }
            else if (operation.ElapsedCpuTime < operation.CpuTimeToComplete)
            {
                //Step 2: Continue Cpu Processing
                ContinueCpuProcessing(operation);
            }
            else if (operation.TempCompleted == false)
            {
                //Step 3: Begin Write
                BeginTempWrite(operation);
            }
            else if (operation.ElapsedIoTime == 0)
            {
                BeginWrite(operation);
            }
            else if (operation.ElapsedIoTime < operation.IoTimeToComplete)
            {
                //Step 4: Continue Writing
                ContinueWriting(operation);
            }

            if (operation.ElapsedIoTime == operation.IoTimeToComplete)
            {
                //Step 5: Operation Finished
                OperationFinished(operation);
            }
        }

        /// <summary>
        /// In handling writes, we must check for WAR conflicts
        /// </summary>
        private void BeginTempWrite(DatabaseOperation operation)
        {
            bool canContinue = true;

            DatabaseObject dbObject = this.DatabaseObjects[operation.ObjectID];
            if (dbObject.ReadingTransactions.Any(i => i != operation.Transaction))
            {
                //Write After Read (WAR) Conflict
                canContinue = WriteAfterReadConflict(operation);
            }
            
            if (canContinue)
            {
                dbObject.WritingTransactions.Add(operation.Transaction);
                operation.IncreaseTempWriteTime();
                this.CurrentSimulationResult.TempWriteTime++;
                operation.MarkTempCompleted();
            }
        }

        private void BeginWrite(DatabaseOperation operation)
        {
            DatabaseObject dbObject = this.DatabaseObjects[operation.ObjectID];
            if (AcquireHardDrive(operation))
            {
                ContinueWriting(operation);
            }
            else
            {
                if (operation.ElapsedBlockedOnHardDriveTime == 0)
                {
                    this.CurrentSimulationResult.HardDriveWriteBlocks++;
                }
                operation.IncreaseBlockedOnHardDriveTime();
                this.CurrentSimulationResult.BlockedOnHardDriveWriteTime++;
            }
        }

        private void ContinueWriting(DatabaseOperation operation)
        {
            operation.IncreaseIoTime();
            this.CurrentSimulationResult.IoUtilization++;

            if (operation.ElapsedIoTime == operation.IoTimeToComplete)
            {
                FinishWrite(operation);
            }
        }

        private void FinishWrite(DatabaseOperation operation)
        {
            ReleaseHardDrive(operation);
        }
        #endregion

        #region Cpu Processing
        private void BeginCpuProcessing(DatabaseOperation operation)
        {
            if (AcquireCpu(operation))
            {
                ContinueCpuProcessing(operation);
            }
            else
            {
                if (operation.ElapsedBlockedOnCpuTime == 0)
                {
                    this.CurrentSimulationResult.CpuBlocks++;
                }
                operation.IncreaseBlockedOnCpuTime();
                this.CurrentSimulationResult.BlockedOnCpuTime++;
            }
        }

        private void ContinueCpuProcessing(DatabaseOperation operation)
        {
            operation.IncreaseCpuTime();
            this.CurrentSimulationResult.CpuUtilization++;

            if (operation.ElapsedCpuTime == operation.CpuTimeToComplete)
            {
                FinishCpuProcessing(operation);
            }
        }

        private void FinishCpuProcessing(DatabaseOperation operation)
        {
            ReleaseCpu(operation);
        }

        private void OperationFinished(DatabaseOperation operation)
        {
            operation.MarkCompleate();
        }
        #endregion

        #region Database Resources
        private bool AcquireCpu(DatabaseOperation operation)
        {
            Cpu cpu = this.Cpus.FirstOrDefault(i => i.CurrentOperation == null);
            if (cpu != null)
            {
                cpu.SetOperation(operation);
            }

            return cpu != null;
        }

        private void ReleaseCpu(DatabaseOperation operation)
        {
            Cpu cpu = this.Cpus.First(i => i.CurrentOperation == operation);
            cpu.ReleaseOperation(operation);
        }

        private bool AcquireHardDrive(DatabaseOperation operation)
        {
            bool success = false;
            int driveIndex = operation.ObjectID * NumberOfHardDrives / NumberOfObjects;
            HardDrive hardDrive = this.HardDrives[driveIndex];
            if (hardDrive.CurrentOperation == null)
            {
                hardDrive.SetOperation(operation);
                success = true;
            }
            return success;
        }

        private void ReleaseHardDrive(DatabaseOperation operation)
        {
            HardDrive hardDrive = this.HardDrives.First(i => i.CurrentOperation == operation);
            hardDrive.ReleaseOperation(operation);
        }
        #endregion

        #region Kill Transaction
        protected virtual void KillTransaction(Transaction transaction)
        {
            transaction.Kill();
            this.CurrentSimulationResult.Deadlocks++;
            DatabaseOperation operation = transaction.Operations.FirstOrDefault(i => !i.Completed);

            if (operation != null)
            {
                foreach (DatabaseResource resource in this.HardDrives.Where(i => i.CurrentOperation == operation))
                {
                    resource.ReleaseOperation(operation);
                }

                foreach (DatabaseResource resource in this.Cpus.Where(i => i.CurrentOperation == operation))
                {
                    resource.ReleaseOperation(operation);
                }
            }

            RemoveReadingAndWriting(transaction);
        }
        #endregion
    }
}
